﻿/*
 * This file is part of MonoSettlers.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://opensettlers.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace MonoSettlers
{
    public enum ResStackType
    {
        /// <summary>
        /// A query is provided with resources.
        /// </summary>
        Query,
        /// <summary>
        /// A provider serves resource queries.
        /// </summary>
        Provider,
    }

    /// <summary>
    /// A resource stack is a integral part of the whole economy. Its a simple but powerful
    /// concept with which any actions seen in Settlers can be driven.
    /// </summary>
    public class GenericResourceStack : PositionTracker
    {
        /// <summary>
        /// There is a maximum number of items supported for ALL resource stacks.
        /// </summary>
        public const Int32 MAX_STACK_SIZE = 8;

        private List<Movable> m_Stack = new List<Movable>();
        private Int32 m_LastCount = 0;

        /// <summary>
        /// Kind of this stack.
        /// </summary>
        public ResStackType Type { get; private set; }
        /// <summary>
        /// Resource typed stored here.
        /// </summary>
        public Resource Resource { get; private set; }
        /// <summary>
        /// Is raised whenever resources are removed or added.
        /// </summary>
        public event DChangeHandler<GenericResourceStack, Int32> OnCountChanged;
        /// <summary>
        /// A placeholder to store visualization data.
        /// </summary>
        public IRenderableVisual Visual { get; set; }
        /// <summary>
        /// For each instance you can set the maximum stack size between
        /// one and <see cref="MAX_STACK_SIZE"/>.
        /// </summary>
        public Int32 MaxCount { get; private set; }
        /// <summary>
        /// The virutal resource count is calculated differently for queries and
        /// providers. For queries it is the sum of <see cref="Available"/> and
        /// <see cref="Requested"/>, for providers its the difference. 
        /// </summary>
        /// <remarks>
        /// Further a query is considered to be full if this value has reached the maximum
        /// stack size, even if for now not all resources are actually added to the
        /// stack, but on their way. A provider can also be empty even if there is
        /// the maximum possible amount of resources stored.
        /// </remarks>
        public Int32 VirtualCount
        {
            get
            {
                if (Type == ResStackType.Query)
                    return Available + Requested;
                else
                    return Available - Requested;
            }
        }
        /// <summary>
        /// The amount of resources building this stack. This does NOT include resource
        /// jobs that will decrease or increase the stack count in near future.
        /// </summary>
        public Int32 Available
        {
            get
            {
                int c = 0;

                foreach (var e in m_Stack)
                {
                    if (e == null)
                        c++;
                }

                return c;
            }
        }
        /// <summary>
        /// The amount of resource job regarding this stack. This does NOT include resources
        /// already placed on the stack.
        /// </summary>
        public Int32 Requested
        {
            get
            {
                int c = 0;

                foreach (var e in m_Stack)
                {
                    if (e != null)
                        c++;
                }

                return c;
            }
        }

        internal GenericResourceStack(ResStackType inType, Resource inResource)
            : this(inType, inResource, MAX_STACK_SIZE)
        {
        }

        internal GenericResourceStack(ResStackType inType, Resource inResource, Int32 inMaxStack)
        {
            if ((inMaxStack < 1) || (inMaxStack > MAX_STACK_SIZE))
                throw new ArgumentOutOfRangeException();

            MaxCount = inMaxStack;
            Type = inType;
            Resource = inResource;
        }

        private void Update()
        {
            if (m_LastCount == VirtualCount)
                return;

            try
            {
                if (OnCountChanged != null)
                    OnCountChanged(this, m_LastCount, VirtualCount);
            }
            finally
            {
                m_LastCount = VirtualCount;
            }
        }

        private bool CheckRange(int inCount)
        {
            if ((inCount < 0) || (inCount > MaxCount))
                return false;

            return true;
        }

        /// <summary>
        /// Adds a resource, depending on stack type, directly, meaning it will increase
        /// <see cref="Available"/>.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">Adding another resource would overflow the stack.</exception>
        public void AddResource()
        {
            AddInternal(null);
        }

        /// <summary>
        /// Adds a resource job, depending on stack type, meaning it will increase
        /// <see cref="Requested"/>.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">Adding another resource would overflow the stack.</exception>
        public void AddJob(Movable inMovable)
        {
            if (inMovable == null)
                throw new ArgumentNullException();

            AddInternal(inMovable);
        }
        
        private void AddInternal(Movable inMovable)
        {
            Debug.Assert (CheckRange(VirtualCount) && CheckRange(Available) && CheckRange(Requested)) ;

            m_Stack.Insert(0, inMovable);

            if (!CheckRange(VirtualCount) || !CheckRange(Available) || !CheckRange(Requested))
            {
                m_Stack.RemoveAt(0);

                throw new ArgumentOutOfRangeException();
            }

            Update();
        }

        /// <summary>
        /// Adds a resource, depending on stack type, directly, meaning it will increase
        /// <see cref="Available"/>.
        /// </summary>
        /// <exception cref="KeyNotFoundException">No resource available to remove.</exception>
        public void RemoveResource()
        {
            RemoveInternal(null);
        }

        /// <summary>
        /// Removes a resource job meaning it will decrease <see cref="Requested"/>.
        /// </summary>
        /// <exception cref="KeyNotFoundException">Given movable was not found.</exception>
        public void RemoveJob(Movable inMovable)
        {
            if (inMovable == null)
                throw new ArgumentNullException();

            RemoveInternal(inMovable);
        }

        private void RemoveInternal(Movable inMovable)
        {
            bool success = false;

            for (int i = 0; i < m_Stack.Count; i++)
            {
                if (m_Stack[i] == inMovable)
                {
                    m_Stack.RemoveAt(i);

                    success = true;
                    break;
                }
            }

            if (!success)
                throw new KeyNotFoundException();

            Update();
        }
    }

    public delegate void DOnAddResourceStack(ResourceManager inManager, GenericResourceStack inStack);
    public delegate void DOnRemoveResourceStack(ResourceManager inManager, GenericResourceStack inStack);

    /// <summary>
    /// The resource manager, as its name implies, takes track of all resource stacks in the game.
    /// It also seeks for settlers carrying resources from provider to queries in a prioritized
    /// way. This is very important, since it ensures that queries with lower <see cref="VirtualCount"/>
    /// will come first, which has a significant positive impact on the whole economy. In the original
    /// game this was not enforced, this combined with the fact that there seemed to be a limit of
    /// resources being carried, it was just a burden if not impossible to get a huge settlement
    /// running properly.
    /// </summary>
    public class ResourceManager
    {
        /// <summary>
        /// All resources and queries are put into this list.
        /// </summary>
        private TopologicalList<GenericResourceStack> m_Resources;
        /// <summary>
        /// All queries will also go here, as long as their <see cref="VirtualCount"/> is
        /// less than their <see cref="MaxStack"/> size. If this is the case you will find
        /// the query in the list with index <see cref="VirtualCount"/>.
        /// </summary>
        private List<GenericResourceStack>[] m_Queries;
        /// <summary>
        /// The associated movable manager.
        /// </summary>
        public MovableManager MovMgr { get; private set; }
        /// <summary>
        /// Is raised whenever a resource stack is added.
        /// </summary>
        public event DOnAddResourceStack OnAddStack;
        /// <summary>
        /// Is raised whenever a resource stack is removed.
        /// </summary>
        public event DOnRemoveResourceStack OnRemoveStack;

        public ResourceManager(MovableManager inMovMgr)
        {
            if (inMovMgr == null)
                throw new ArgumentNullException();

            MovMgr = inMovMgr;
            m_Resources = new TopologicalList<GenericResourceStack>(10, inMovMgr.Size, inMovMgr.Size);
            m_Queries = new List<GenericResourceStack>[GenericResourceStack.MAX_STACK_SIZE];

            for (int i = 0; i < m_Queries.Length; i++)
            {
                m_Queries[i] = new List<GenericResourceStack>();
            }
        }

        /// <summary>
        /// Adds a new resource stack with the given parameters and raises <see cref="OnAddStack"/>.
        /// Why is there no instance returned? The reson is plain and simple. This method is also
        /// called internally and it is crucial to do all postprocessings for a resource stack,
        /// like attaching a visual for rendering, in the mentioned event.
        /// </summary>
        /// <exception cref="ArgumentException">There is already a resource or the given position is not walkable.</exception>
        public void AddResourceStack(CyclePoint inPosition, ResStackType inType, Resource inResource, Int32 inMaxStack)
        {
            AddResourceStackInternal(inPosition, inType, inResource, inMaxStack);
        }

        /// <summary>
        /// Same as <see cref="AddResourceStack"/>, but returns the created stack. Since this is a
        /// private method, it is assumed that the caller knows what he is doing with the returned
        /// instance.
        /// </summary>
        private GenericResourceStack AddResourceStackInternal(CyclePoint inPosition, ResStackType inType, Resource inResource, Int32 inMaxStack)
        {
            if (m_Resources.EnumAt(inPosition.ToPoint()).Count() > 0)
                throw new ArgumentException("There is already a resource at the given position.");

            if (!MovMgr.IsWalkable(inPosition.ToPoint(), 0))
                throw new ArgumentException("Given position is not walkable.");

            GenericResourceStack stack = new GenericResourceStack(inType, inResource, (inMaxStack > 0) ? inMaxStack : GenericResourceStack.MAX_STACK_SIZE);

            stack.Position = inPosition;

            if (stack.Type == ResStackType.Query)
            {
                stack.OnCountChanged += OnStackCountChanged;

                m_Queries[0].Add(stack);
            }

            m_Resources.Add(stack);

            if (OnAddStack != null)
                OnAddStack(this, stack);

            return stack;
        }

        /// <summary>
        /// For resource queries we need to keep track of their stack size, since we have
        /// to relocate them appropriately in the priority queue.
        /// </summary>
        private void OnStackCountChanged(GenericResourceStack inSender, int inOldValue, int inNewValue)
        {
            if (inOldValue < inSender.MaxCount)
            {
                // remove from old queue
                if (!m_Queries[inOldValue].Remove(inSender))
                    throw new ApplicationException("Resource query is not registered in queue.");
            }

            if (inNewValue < inSender.MaxCount)
            {
                // add to new queue
                m_Queries[inNewValue].Add(inSender);
            }
        }

        /// <summary>
        /// If no resource exists at the given grid point, a new resource provider is added and
        /// filled. Otherwise the existing stack is used if it is of the same resource type.
        /// If <paramref name="inCount"/> exceeds the remaining stack capacity, the method enumerates
        /// around the given point and repeats the task until all desired resources are placed.
        /// </summary>
        /// <remarks>
        /// This is an important method also demonstrating how powerful our lightweight resource
        /// concept is. With almost no effort we can simulate resource drops in the same way they
        /// are done in the original game.
        /// </remarks>
        /// <param name="inAround">You may also pass unwalkable spots. The method enumerates until it finds one.</param>
        /// <param name="inResource">The resource type being dropped.</param>
        /// <param name="inCount">Amount of resources being dropped.</param>
        public void DropResource(Point inAround, Resource inResource, int inCount)
        {
            MovMgr.EnumFreeSpacesAround(inAround, 1, 1, (pos) =>
            {
                IEnumerable<GenericResourceStack> stackEnum = m_Resources.EnumAt(pos);
                GenericResourceStack stack;

                if (stackEnum.Count() > 0)
                {
                    // merge with existing stacks
                    stack = stackEnum.First();

                    if ((stack.Type == ResStackType.Query) || (stack.Resource != inResource))
                        return WalkResult.NotFound;
                }
                else
                {
                    // add a new resource provider
                    stack = AddResourceStackInternal(CyclePoint.FromGrid(pos), ResStackType.Provider, inResource, 0);
                }

                int freeCount = stack.MaxCount - stack.VirtualCount;

                if (freeCount <= 0)
                    return WalkResult.NotFound;

                freeCount = Math.Min(freeCount, inCount);

                for (int i = 0; i < freeCount; i++)
                {
                    stack.AddResource();
                }

                inCount -= freeCount;

                if (inCount <= 0)
                    return WalkResult.Success;
                else
                    return WalkResult.NotFound;
            });
        }

        /// <summary>
        /// Removes the given stack. Please keep in mind that this method is also called
        /// internally and it is crucial that you put ALL additional code required to release
        /// the resource stack into the event handler <see cref="OnRemoveStack"/>, like
        /// detaching it from the render pipeline.
        /// </summary>
        /// <exception cref="ArgumentNullException">No resource given.</exception>
        /// <exception cref="ApplicationException">Resource does not exist.</exception>
        public void RemoveResource(GenericResourceStack inResource)
        {
            if (inResource == null)
                throw new ArgumentNullException();

            m_Resources.Remove(inResource);

            // remove from query queue
            if (inResource.Type == ResStackType.Query)
            {
                if (inResource.VirtualCount < GenericResourceStack.MAX_STACK_SIZE)
                {
                    if (!m_Queries[inResource.VirtualCount].Remove(inResource))
                        throw new ApplicationException("Resource query is not registered in queue.");
                }
            }
        }

        /// <summary>
        /// Will do all the job planning.
        /// </summary>
        public void AddOneCycle()
        {
            if ((MovMgr.CurrentCycle % (double)CyclePoint.CYCLE_MILLIS) != 0)
                return;

            /*
             * Currently queries are degraded to the next lower priority whenever they
             * got a new job assigned. This may lead to a whole bunch of settlers running
             * at the same time for the same providers and queries. Even if this is the
             * desired behavior in production mode, it may not be desired when debugging
             * issues in path engine or resource manager.
             * 
             * So by forking (outcommented) the internal query lists, only one job will
             * be assigned per query per cycle.
             */
            //var queueArray = new GenericResourceStack[m_Queries.Length][];

            //for(int i = 0; i < queueArray.Length; i++)
            //{
            //    queueArray[i] = m_Queries[i].ToArray();
            //}

            foreach (var queue in m_Queries)
            {
                foreach (var query in queue.ToArray())
                {
                    Debug.Assert(query.Type == ResStackType.Query);

                    // search for matching provider
                    GenericResourceStack provider = null;

                    m_Resources.EnumAround(query.Position.ToPoint(), (prov) =>
                    {
                        if (prov.Type != ResStackType.Provider)
                            return WalkResult.NotFound;

                        if (prov.Resource != query.Resource)
                            return WalkResult.NotFound;

                        if(prov.VirtualCount <= 0)
                            return WalkResult.NotFound;

                        provider = prov;

                        return WalkResult.Success;
                    });

                    if (provider == null)
                        continue;

                    // find settler around provider
                    Movable movable = null;

                    MovMgr.EnumMovablesAround(provider.Position.ToPoint(), (mov) =>
                    {
                        if (mov.HasJob)
                            return WalkResult.NotFound;

                        movable = mov;

                        return WalkResult.Success;
                    });

                    if (movable == null)
                        continue;

                    // create settler job
                    provider.AddJob(movable);
                    query.AddJob(movable);

                    /*
                     * The following may look weird. It is important to notice that it is
                     * an asynchronous task. The first level is called when the settler has reached
                     * the provider. Then it picks up the resource and walks to the query. If it
                     * has reached the query, the resource is finally dropped and the settler is idle
                     * again. If anything happend on its way, the settlers can drop the resource and become
                     * idle (for example if someone stop house building, all settlers carrying resources
                     * to it are immediately stopped and drop them). Since stopping a settlers carrying
                     * a resource will lead to the resource being dropped automatically, we have to be
                     * careful not to create scenarios is which resources can be forked (cheating).
                     * 
                     * The following again shows the powerful concepts provided by resource manager and
                     * path engine.
                     */
                    MovMgr.SetPath(movable, provider.Position.ToPoint(), (successProv) =>
                    {
                        provider.RemoveJob(movable);
                        query.RemoveJob(movable);

                        if (!successProv)
                        {
                            movable.Stop();

                            return;
                        }

                        // pick up resource
                        provider.RemoveResource();
                        movable.Carrying = provider.Resource;

                        // walk to next resource
                        query.AddJob(movable);

                        MovMgr.SetPath(movable, query.Position.ToPoint(), (successQuery) =>
                        {
                            query.RemoveJob(movable);

                            if (!successQuery)
                            {
                                movable.Stop();

                                return;
                            }

                            // add resource to query
                            query.AddResource();
                            movable.Carrying = null;

                            // finish job
                            movable.Stop();
                        });
                    });
                }
            }
        }
    }
}
